// ?raw 是vite用于加载原始文件的方式
import css from "./index.scss?raw";
import { h, render } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import {
  AreaCodeItem,
  AreaCodeKey,
  ChoseItem,
  Lang,
  Props,
  STATICAREA,
  ZH,
} from "./data";
import {
  paseOptionByLang,
  getNewOptions,
  setUsed,
  parseUsed,
  cloneAreaCode,
  parseShowLabel,
  parseCode,
  findData,
  sortCode,
} from "./helper";
import { debounce, getUid } from "../../common/utils";

const tagName = "area-select";
const closeDirc = new Map();

function TPL(props: Props) {
  const { options, change, uid, oldValue, ins } = props;
  // 这里从getProps上拿是因为在vue和react里面，组件是先挂载再render，render后这个标签属性才会生效
  const { lang, readonly, disabled, filedLabel, parseLabel, width, height } =
    ins.getProps();
  const inputRef = useRef(null);
  const ulRef = useRef<HTMLUListElement>(null);
  // 暂存原始数据，方便过滤用
  ins._originOptions = options;
  const [show, setShow] = useState(false); // 显示/隐藏选项
  const [value, setValue] = useState(oldValue); // 输入框显示的值
  const [newOptions, setNewOptions] = useState(options); // 选项
  const [optI, setOpt] = useState(-1); // 用于键盘控制选项的

  // 监听键盘事件
  useEffect(() => {
    const keyCodeHandler = (e: KeyboardEvent) => {
      const stop = () => {
        e.preventDefault();
        e.stopPropagation();
      };
      // 保持选项在视口
      const setInView = (i: number) => {
        setTimeout(() => {
          console.log(i);
          const el = ulRef.current?.childNodes[i] as HTMLLIElement;
          const { top, bottom } = ulRef.current?.getBoundingClientRect() || {};
          const { top: liTop, bottom: liBottom } =
            el.getBoundingClientRect() || {};
          if (!top || !bottom) {
            return;
          }
          if (liTop < top) {
            // 在上面
            el.scrollIntoView({
              block: "start",
            });
          } else if (liBottom > bottom) {
            // 下面
            el.scrollIntoView({
              block: "end",
            });
          } else {
            // 在ul的视口内，目前不需要
          }
        }, 10);
      };
      if (!show) return;
      switch (e.key) {
        case "ArrowUp":
          stop();
          if (optI <= 0) return;
          setOpt(optI - 1);
          setInView(optI - 1);
          break;
        case "ArrowDown":
          stop();
          if (optI >= newOptions.length - 1) return;
          setOpt(optI + 1);
          setInView(optI + 1);
          break;
        case "Enter":
          stop();
          const area = newOptions[optI];
          chose(area);
          break;
      }
    };
    document.addEventListener("keydown", keyCodeHandler);
    return () => {
      document.removeEventListener("keydown", keyCodeHandler);
    };
  }, [show, optI, newOptions]);
  // 监听点击外面
  useEffect(() => {
    const clickOutside = () => {
      if (show) {
        // 再加一个验证输入框恢复
        setValue(value);
        setShow(false);
      }
    };
    document.addEventListener("mousedown", clickOutside);
    return () => {
      document.removeEventListener("mousedown", clickOutside);
    };
  }, [show]);
  // 发请求更新和处理手动更新
  useEffect(() => {
    getNewOptions(lang).then((arr) => {
      if (arr.length) {
        // 处理label、排序、更新
        paseOptionByLang(arr, lang)
        sortCode(arr, lang);
        ins._originOptions = arr;
        setNewOptions(arr);
      }
    });
    // 加入关闭其他组件的方法
    closeDirc.set(uid, () => setShow(false));
    // 用于适配v-model的情况
    ins._updArea = (code: string) => {
      const realCode = parseCode(code);
      const area = findData(options, "areaCodeForPlusSign", realCode)[0];
      if (area) {
        chose(area, true);
      } else {
        // 没找到选项的情况
        setValue(code);
      }
    };
    while(ins._updCache?.length) {
      const val = ins._updCache.shift()
      ins._updArea(val)
    }
    return () => {
      closeDirc.delete(uid);
    };
  }, []);
  // 输入
  const inputVal = (e: InputEvent) => {
    e.preventDefault();
    e.stopPropagation();
    const val = (e.target as HTMLInputElement).value;
    setValue(val);
    const filterArr = ins._originOptions.filter((v: AreaCodeItem) =>
      v.label?.includes(val)
    );
    parseUsed(filterArr);
    setNewOptions(filterArr);
  };
  // 聚焦展开选项前，先关闭其他正在开启的
  const inputFocus = () => {
    const cbs = closeDirc.values();
    for (const cb of cbs) {
      cb();
    }
    // 每次都更新常用
    parseUsed(ins._originOptions);
    setNewOptions(ins._originOptions);
    setShow(true);
    setOpt(-1);
    setTimeout(() => {
      const el = ulRef.current;
      el?.scrollTo(0, 0);
    });
  };
  // 选择, 可能不需要调用回调
  const chose = (area: AreaCodeItem, notEmit = false) => {
    const label = parseShowLabel(area, {
      filedLabel,
      parseLabel,
    });
    setValue(label || "");
    setShow(false);
    const data: ChoseItem = Object.assign(area, {
      areaCode: area.areaCodeForZero.replaceAll("00", ""),
    });
    setUsed(area.simpleCode);
    if (!notEmit) {
      change(data);
      (inputRef.current as HTMLIFrameElement | null)?.blur();
    }
  };
  // 移入
  const moveUp = debounce((i: number) => {
    setOpt(i);
  }, 10);
  // 计算样式
  const getPanelStyle = () => {
    const styles: any = {};
    width && (styles.width = width);
    height && (styles.height = height);
    return styles;
  };
  return (
    <>
      <div
        className={`area-select-panel ${show ? "focus" : ""}`}
        onMouseDown={(e) => e.stopPropagation()}
        style={getPanelStyle()}
      >
        <input
          type="text"
          value={value}
          ref={inputRef}
          onInput={(e) => inputVal(e)}
          onFocus={() => inputFocus()}
          readOnly={readonly}
          disabled={disabled}
        />
        <div class={`flex-c-c icon ${show ? "icon-act" : ""} `}>
          <slot name={"icon"}>
            <div class={"def-icon"}></div>
          </slot>
        </div>
        <ul ref={ulRef} class={show ? "" : "hide"}>
          {newOptions.map((area, index) => {
            // areaCodeForPlusSign, simpleCode,
            const { label, isUsed } = area;
            return (
              <li
                key={label}
                onMouseDown={() => chose(area)}
                // onMouseEnter={() => moveUp(index)}
                onMouseMove={() => moveUp(index)}
                className={index === optI ? "act" : ""}
              >
                <span className="txt-l">{label || ""}</span>
                <span className="txt-r">
                  {isUsed ? (lang === ZH ? "最近常用" : "Recently used") : ""}
                </span>
              </li>
            );
          })}
        </ul>
      </div>
    </>
  );
}

export class AreaSelect extends HTMLElement {
  originCode: AreaCodeItem[] = cloneAreaCode(STATICAREA);
  _originOptions: AreaCodeItem[] = [] // 原始的选项,请求后更新
  language: Lang = ZH;
  uid: string = getUid(); // 用于区分唯一的id
  filedLabel: AreaCodeKey | null = null;
  parseLabel: (() => string) | null = null;
  _updArea?: (a: string) => void; // TPL里面注册的
  _updCache: string[] = []; // 首次更新暂存值
  _value: string = "";
  value: any;
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    sortCode(this.originCode, this.language); // 根据语言排序
    
    const styleEl = document.createElement("style");
    styleEl.innerHTML = css;
    this.shadowRoot?.append(styleEl);
    // setTimeout(() => {
      
    // }, 10);
    // 添加事件
    this.setVModelEffect();
    this.init();
  }
  async init() {
    // 给到组件的属性
    const tagProps = this.getProps()
    const prop = {
      options: this.originCode,
      uid: this.uid,
      change: (val: ChoseItem) => this.change(val),
      oldValue: parseCode(this.value || tagProps.value), // 兼容原生，原生的
      ins: this,
      // lang: (this.getAttribute("lang") as Lang) || ZH,
      // filedLabel: this.getAttribute("filed-label") as AreaCodeKey,
      // parseLabel: this.getAttribute("parse-label") as (() => string) | null,
      ...tagProps,
    } as Props
    paseOptionByLang(this.originCode, prop.lang);
    // 区号转成显示用的label
    {
      const { options, oldValue, filedLabel, parseLabel } = prop;
      const area = findData(options, "areaCodeForPlusSign", oldValue)[0];
      if (area) {
        prop.oldValue =
          parseShowLabel(area, { filedLabel, parseLabel }) || oldValue;
      }
    }
    const tree = h(TPL, prop);
    render(tree, this.shadowRoot!);
    
  }
  // 获取组件属性
  getProps() {
    const atts = [
      {
        key: "lang",
        def: ZH,
      },
      {
        key: "filed-label",
        def: "label",
      },
      {
        key: "parse-label",
        def: null,
      },
      {
        key: "width",
        def: "200px",
      },
      {
        key: "height",
        def: "30px",
      },
      {
        key: "disabled",
        def: false,
      },
      {
        key: "readonly",
        def: false,
      },
      {
        key: "value",
        def: '',
      },
    ];
    const dir: {
      [key: string]: any;
    } = {};
    atts.forEach((v) => {
      const { key, def } = v;
      dir[key] = this.getAttribute(key) || def;
    });
    return dir;
  }
  // 事件change
  change(val: ChoseItem) {
    // 这里不直接修改value触发inpu是因为更新是异步，_value的值还没更新就先change了
    // 所以要么直接更新_value再触发或者把更新放到宏任务里面延迟执行（页面更新也属于宏任务）
    this._value = String(Number(val.areaCodeForZero));
    const event = new CustomEvent("change", {
      detail: val,
      bubbles: true, // 事件是否冒泡
      composed: true, // 事件是否穿透 Shadow DOM 边界
    });
    this.dispatchEvent(event);
    this.dispatchEvent(new CustomEvent("input", { detail: val }));
  }

  // 处理处理输入问题
  setVModelEffect() {
    Object.defineProperty(this, "value", {
      get() {
        return this._value;
      },
      set: (newValue) => {
        if (this.value === newValue) {
          return;
        }
        this._value = newValue;
        // 这个更新方法是在TPL里面注册的，用于更新这个值; 
        // 由于首次的赋值这里可能还没更新，所以需要先把操作存起来
        if(this._updArea) {
          this._updArea(newValue);
          this.dispatchEvent(new CustomEvent("input", { detail: null }));
        } else {
          this._updCache.push(newValue)
        }
      },
    });
  }

  // 对外查询方法
  findData(key: AreaCodeKey, value: string) {
    return this._originOptions?.filter((a) => a[key] === value);
  }
}

customElements.define(tagName, AreaSelect);
